SPB Git

spb/autoscholar

Public
7.6 KB · 187 lines tsx
1import { getRun, listEvents } from "@autoscholar/core";2import { redact } from "@autoscholar/observability";3import { STAGES } from "@autoscholar/schemas";4import Link from "next/link";5import { notFound } from "next/navigation";6import { StatusBadge } from "@/components/StatusBadge";7import { disciplineLabel, fmtBytes, fmtDateTime, fmtUsd, modeLabel, shortHash } from "@/lib/format";8import { db } from "@/lib/server/db";9import { LiveTimeline } from "./LiveTimeline";10import { RunActions } from "./RunActions";1112export const dynamic = "force-dynamic";1314export default async function RunDetail({ params }: { params: Promise<{ id: string }> }) {15  const { id } = await params;16  const d = db();17  let data: Awaited<ReturnType<typeof getRun>>;18  try {19    data = await getRun(d, id);20  } catch {21    notFound();22  }23  const { run, attempts, stages, artifacts, lease, gates } = data;24  const initialEvents = (await listEvents(d, id, 0, 500)).map((e) =>25    redact({26      seq: e.seq,27      at: e.at.toISOString(),28      level: e.level,29      kind: e.kind,30      stage: e.stage,31      message: e.message,32      data: e.data,33    }),34  );35  const latestAttempt = attempts.at(-1);36  const stageMap = new Map(stages.filter((s) => s.attemptId === latestAttempt?.id).map((s) => [s.stage, s]));37  const latestArtifacts = new Map<string, (typeof artifacts)[number]>();38  for (const a of artifacts) if (!latestArtifacts.has(a.path)) latestArtifacts.set(a.path, a);39  const manifest = run.manifest as Record<string, unknown>;40  return (41    <div className="space-y-6">42      <header className="flex flex-wrap items-start justify-between gap-3">43        <div>44          <p className="eyebrow">45            <Link href="/admin/runs">Runs</Link>46          </p>47          <h1 className="title-2 mt-1">{run.title}</h1>48          <p className="muted mt-1 flex flex-wrap items-center gap-2 text-sm">49            <StatusBadge status={run.status} />50            <span>51              {disciplineLabel(run.discipline)} · {modeLabel(run.researchMode)}52              {run.secondaryModes.length ? ` (+${run.secondaryModes.map(modeLabel).join(", ")})` : ""}53            </span>54            <span className="font-mono text-xs">{run.id}</span>55          </p>56        </div>57        <RunActions runId={run.id} status={run.status} paperId={run.paperId} />58      </header>5960      <section className="grid grid-cols-2 gap-3 sm:grid-cols-4 lg:grid-cols-6">61        {[62          ["Attempt", `${run.currentAttemptNo}/${run.maxAttempts}`],63          ["Stage", run.currentStage ?? "—"],64          ["Cost", `${fmtUsd(run.totalCostUsd)} / ${fmtUsd(run.maxBudgetUsd)}`],65          ["Runtime cap", `${run.maxRuntimeMinutes} min`],66          ["Worker", lease ? `lease #${lease.fencingToken}` : latestAttempt?.workerId ? "released" : "—"],67          ["Auto-publish", run.autoPublish ? "yes" : "no"],68        ].map(([l, v]) => (69          <div key={l} className="surface p-3">70            <p className="label">{l}</p>71            <p className="mt-1 font-mono text-sm">{v}</p>72          </div>73        ))}74      </section>7576      <section aria-labelledby="stages">77        <h2 id="stages" className="title-3">78          Stage progress79        </h2>80        <ol className="mt-2 grid gap-1 sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-6">81          {STAGES.map((s) => {82            const ex = stageMap.get(s.key);83            const st =84              ex?.status ??85              (s.key === "quality" && gates.length86                ? "completed"87                : s.key === "publication" && run.status === "published"88                  ? "completed"89                  : "pending");90            return (91              <li key={s.key} className="surface flex items-center justify-between gap-2 px-3 py-2 text-xs">92                <span>93                  <span className="subtle font-mono">{s.index}</span> {s.name}94                </span>95                <StatusBadge status={st} />96              </li>97            );98          })}99        </ol>100      </section>101102      <div className="grid gap-6 lg:grid-cols-12">103        <section className="lg:col-span-7" aria-labelledby="timeline">104          <h2 id="timeline" className="title-3">105            Timeline106          </h2>107          <LiveTimeline108            runId={run.id}109            initial={initialEvents}110            active={!["published", "failed", "cancelled"].includes(run.status)}111          />112        </section>113        <div className="space-y-6 lg:col-span-5">114          <section className="surface p-4">115            <h2 className="title-3">Artifacts ({latestArtifacts.size})</h2>116            {latestArtifacts.size === 0 ? (117              <p className="muted mt-2 text-sm">No artifacts uploaded yet.</p>118            ) : (119              <ul className="mt-2 max-h-96 space-y-1 overflow-auto text-xs">120                {[...latestArtifacts.values()].map((a) => (121                  <li key={a.id} className="flex items-center justify-between gap-2 border-b py-1 hairline">122                    <a href={`/api/admin/runs/${run.id}/artifacts/${a.path}`} className="font-mono break-all">123                      {a.path}124                    </a>125                    <span className="subtle whitespace-nowrap">126                      v{a.version} · {fmtBytes(a.bytes)} · {shortHash(a.sha256, 8)}127                      {a.synthetic ? " · synthetic" : ""}128                    </span>129                  </li>130                ))}131              </ul>132            )}133          </section>134          <section className="surface p-4">135            <h2 className="title-3">Attempts</h2>136            <ul className="mt-2 space-y-1 text-xs">137              {attempts.map((a) => (138                <li key={a.id} className="flex justify-between gap-2">139                  <span>140                    #{a.attemptNo} <StatusBadge status={a.status} />{" "}141                    {a.resumeFromStage ? `resumed after ${a.resumeFromStage}` : ""}142                  </span>143                  <span className="subtle">144                    {fmtDateTime(a.startedAt)} → {a.endedAt ? fmtDateTime(a.endedAt) : "…"} ·{" "}145                    {fmtUsd(a.costUsd)}146                  </span>147                  {a.error && (148                    <span className="block" style={{ color: "var(--color-danger-500)" }}>149                      {a.error}150                    </span>151                  )}152                </li>153              ))}154            </ul>155          </section>156          <section className="surface p-4">157            <h2 className="title-3">Immutable manifest</h2>158            <dl className="mt-2 grid grid-cols-[max-content_1fr] gap-x-3 gap-y-0.5 text-xs">159              {Object.entries(manifest).map(([k, v]) => (160                <div key={k} className="contents">161                  <dt className="muted">{k}</dt>162                  <dd className="font-mono break-all">{typeof v === "string" ? v : JSON.stringify(v)}</dd>163                </div>164              ))}165              <dt className="muted">manifestSha256</dt>166              <dd className="font-mono break-all">{run.manifestSha256}</dd>167            </dl>168          </section>169          {gates.length > 0 && (170            <section className="surface p-4">171              <h2 className="title-3">Latest quality evaluation</h2>172              <p className="muted text-xs">173                {gates.filter((g) => g.status === "pass").length} pass ·{" "}174                {gates.filter((g) => g.status === "fail").length} fail ·{" "}175                {gates.filter((g) => g.status === "warn").length} warn176              </p>177              <Link href={`/admin/quality/${run.id}`} className="btn btn-sm mt-2">178                Open quality report179              </Link>180            </section>181          )}182        </div>183      </div>184    </div>185  );186}187
Files · b1229548352bd25ab6d221039426fecbffb2b0fa
Menu

Theme